<?php

declare(strict_types=1);

namespace App\Repository;

use PDO;
use RuntimeException;

final readonly class WebhookRequestRepository
{
    public function __construct(private PDO $pdo) {}

    /**
     * @param array<string, mixed> $queryParams
     * @param array<string, list<string>> $headers
     *
     * @return array{id:int,webhook_id:int,method:string,scheme:string,host:string,path:string,query_params:array<string,mixed>,headers:array<string,list<string>>,body:?string,ip_address:?string,user_agent:?string,created_at:string}
     */
    public function record(
        int $webhookId,
        string $method,
        string $scheme,
        string $host,
        string $path,
        array $queryParams,
        array $headers,
        string|null $body,
        string|null $ipAddress,
        string|null $userAgent,
    ): array {
        $statement = $this->pdo->prepare(
            'INSERT INTO webhook_requests (webhook_id, method, scheme, host, path, query_params, headers, body, ip_address, user_agent)
             VALUES (:webhook_id, :method, :scheme, :host, :path, CAST(:query_params AS JSONB), CAST(:headers AS JSONB), :body, :ip_address, :user_agent)
             RETURNING id, webhook_id, method, scheme, host, path, query_params, headers, body, ip_address, user_agent, created_at',
        );
        $statement->bindValue(':webhook_id', $webhookId, PDO::PARAM_INT);
        $statement->bindValue(':method', strtoupper($method), PDO::PARAM_STR);
        $statement->bindValue(':scheme', $scheme, PDO::PARAM_STR);
        $statement->bindValue(':host', $host, PDO::PARAM_STR);
        $statement->bindValue(':path', $path, PDO::PARAM_STR);
        $statement->bindValue(':query_params', json_encode($queryParams, JSON_THROW_ON_ERROR), PDO::PARAM_STR);
        $statement->bindValue(':headers', json_encode($headers, JSON_THROW_ON_ERROR), PDO::PARAM_STR);
        $statement->bindValue(':body', $body, $body === null ? PDO::PARAM_NULL : PDO::PARAM_STR);
        $statement->bindValue(':ip_address', $ipAddress, $ipAddress === null ? PDO::PARAM_NULL : PDO::PARAM_STR);
        $statement->bindValue(':user_agent', $userAgent, $userAgent === null ? PDO::PARAM_NULL : PDO::PARAM_STR);
        $statement->execute();

        /** @var array{id:int,webhook_id:int,method:string,scheme:string,host:string,path:string,query_params:string,headers:string,body:?string,ip_address:?string,user_agent:?string,created_at:string}|false $row */
        $row = $statement->fetch();

        if ($row === false) {
            throw new RuntimeException('Failed to record webhook request.');
        }

        return $this->decodeRow($row);
    }

    /**
     * @return list<array{id:int,method:string,path:string,created_at:string}>
     */
    public function listForWebhook(int $webhookId, int $limit = 50): array
    {
        $statement = $this->pdo->prepare(
            'SELECT id, method, path, created_at FROM webhook_requests WHERE webhook_id = :webhook_id ORDER BY created_at DESC LIMIT :limit',
        );
        $statement->bindValue(':webhook_id', $webhookId, PDO::PARAM_INT);
        $statement->bindValue(':limit', $limit, PDO::PARAM_INT);
        $statement->execute();

        /** @var list<array{id:int,method:string,path:string,created_at:string}> $rows */
        $rows = $statement->fetchAll();

        return $rows;
    }

    public function deleteForWebhook(int $webhookId): void
    {
        $statement = $this->pdo->prepare('DELETE FROM webhook_requests WHERE webhook_id = :webhook_id');
        $statement->bindValue(':webhook_id', $webhookId, PDO::PARAM_INT);
        $statement->execute();
    }

    /**
     * @return array{id:int,webhook_id:int,method:string,scheme:string,host:string,path:string,query_params:array<string,mixed>,headers:array<string,list<string>>,body:?string,ip_address:?string,user_agent:?string,created_at:string}|null
     */
    public function find(int $webhookId, int $requestId): array|null
    {
        $statement = $this->pdo->prepare(
            'SELECT id, webhook_id, method, scheme, host, path, query_params, headers, body, ip_address, user_agent, created_at
             FROM webhook_requests WHERE webhook_id = :webhook_id AND id = :id',
        );
        $statement->bindValue(':webhook_id', $webhookId, PDO::PARAM_INT);
        $statement->bindValue(':id', $requestId, PDO::PARAM_INT);
        $statement->execute();

        /** @var array{id:int,webhook_id:int,method:string,scheme:string,host:string,path:string,query_params:string,headers:string,body:?string,ip_address:?string,user_agent:?string,created_at:string}|false $row */
        $row = $statement->fetch();

        if ($row === false) {
            return null;
        }

        return $this->decodeRow($row);
    }

    /**
     * @param array{id:int,webhook_id:int,method:string,scheme:string,host:string,path:string,query_params:string,headers:string,body:?string,ip_address:?string,user_agent:?string,created_at:string} $row
     *
     * @return array{id:int,webhook_id:int,method:string,scheme:string,host:string,path:string,query_params:array<string,mixed>,headers:array<string,list<string>>,body:?string,ip_address:?string,user_agent:?string,created_at:string}
     */
    private function decodeRow(array $row): array
    {
        /**
         * @var array<string, mixed> $queryParams
         */
        $queryParams = json_decode($row['query_params'], true, 512, JSON_THROW_ON_ERROR);
        /**
         * @var array<string, list<string>> $headers
         */
        $headers = json_decode($row['headers'], true, 512, JSON_THROW_ON_ERROR);

        return [
            'id' => (int) $row['id'],
            'webhook_id' => (int) $row['webhook_id'],
            'method' => $row['method'],
            'scheme' => $row['scheme'],
            'host' => $row['host'],
            'path' => $row['path'],
            'query_params' => $queryParams,
            'headers' => $headers,
            'body' => $row['body'],
            'ip_address' => $row['ip_address'],
            'user_agent' => $row['user_agent'],
            'created_at' => $row['created_at'],
        ];
    }
}
